﻿
namespace Behemoth.SharePoint.ContentTypes {

    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Text;
    //using System.Xml;
    using System.Xml.Linq;

    using Microsoft.SharePoint;
    using Microsoft.SharePoint.Utilities;

    using Behemoth.SharePoint;
    using Behemoth.SharePoint.Configuration;
    using Behemoth.SharePoint.Logging;
    using Behemoth.SharePoint.Services.WebServices;
    using Behemoth.Xml.Linq;

    /// <summary>
    /// This class contains some methods that are useful when creating advanced Features
    /// that deploy site columns and content types. Here are some things it solves:
    /// 1) Updates the ghosted and unghosted version of a content type when the feature is activated.
    /// 2) Allows you to make a Web scoped content type feature based on an element file.
    /// 3) Propagates changes to existing list content types upon activation using a timer job.
    /// </summary>
    /// <remarks>
    /// This version introduces the use of System.Xml.Linq.
    /// If you are stuck in .NET 3.0, living on the moon, or
    /// want to use a class that has been battle tested a little more,
    /// use SPContentTypeFeatureTools instead.
    /// </remarks>
    public class SPContentTypeFeatureToolsX {

        #region RefreshListContentTypes Event

        public static event ListContentTypeRefreshEventHandler RefreshListContentTypes;

        public static void RemoveAllRefreshListContentTypes() {
            RefreshListContentTypes = null;
        }

        //RefreshListContentTypes += new ListContentTypeRefreshEventHandler(Default_RefreshListContentTypes);
        public static void OnRefreshListContentTypes(SPWeb web, List<string> contentTypeNames) {
            if (RefreshListContentTypes != null) {
                ListContentTypeRefreshEventArgs e = new ListContentTypeRefreshEventArgs(contentTypeNames);
                RefreshListContentTypes(web, e);
            }
        }

        public static void DoRefreshListContentTypes(object web, ListContentTypeRefreshEventArgs args) {
            SPWeb targetWeb = web as SPWeb;
            if (web == null)
                throw new ArgumentNullException("Expecting a valid object of type SPWeb.", "web");
            if (args.UseTimerJob) {
                // runs ContentTypeRefreshTimerJob.DoRefreshListContentTypes as a run-once timer job
                ContentTypeRefreshTimerJob.CreateInstance(targetWeb, args);
            } else {
                ContentTypeRefreshTimerJob.DoRefreshListContentTypes(targetWeb, args);
            }

        }

        #endregion

        #region Content Types

        /// <summary>
        /// 
        /// </summary>
        /// <param name="web">
        /// Web you want to create content types for, 
        /// or use SPSite.RootWeb for site collection level.
        /// </param>
        /// <param name="elementFeatureAndFile"></param>
        public static void EnsureContentTypes(SPWeb web, string elementFeatureAndFile) {
            XElement elementDoc = SPFeatureXmlTools.XGetConfigFile(elementFeatureAndFile);
            EnsureContentTypes(web, elementDoc);
        }

        public static void EnsureContentTypes(SPWeb web, XElement elementDoc) {
            // get all ContentType nodes in /ContentTypes/ContentType
            // attribs available are... 
            //  Name (CT Display Name, is also in element file)
            //  ID (the bizarre "octet" 0x0000 format, is also in element file)
            //  Group (string like "_Hidden", is also in element file)
            //  Description (string, is also in element file)
            //  NewDocumentControl (string)
            //  Scope (a SP web address)
            //  Version (a whole number from 0 to ...)
            //  RequireClientRenderingOnNew (TRUE or FALSE)
            XElement currentContentTypeDefs = SPWebServicesClientFactory.XGetContentTypesCollection(web);

            // loop through element file
            List<string> cTypesNeedUpdate = new List<string>();
            List<XElement> cTypes = elementDoc.GetAllElementsOfType("ContentType");
            foreach (XElement cType in cTypes) {

                // for currentContentTypesDoc find /ContentTypes/ContentType[@ID='']
                string cTypeID = cType.Attribute("ID").Value;
                string cTypeName = cType.Attribute("Name").Value;

                //string queryExistingCypeByID = string.Format("/ContentTypes/ContentType[@ID='{0}']", cTypeID);
                //XmlNodeList qryExistingCType = currentContentTypeDefs.SelectNodes(queryExistingCypeByID); // nsmgr
                List<XElement> qryExistingCType = (
                    from XElement ct in currentContentTypeDefs.Descendants() 
                    where ct.Name.LocalName == "ContentType" && ct.Attribute("ID").Value == cTypeID
                    select ct
                ).ToList();
                // TODO: we could do this by name and group too to prevent weird conflcits...

                bool cTypeExists = (qryExistingCType != null && qryExistingCType.Count > 0);
                if (cTypeExists) {
                    // count up the fields in the current content type
                    // seperate into new fields and updated fields
                    // call UpdateContentType
                    XElement existingCType = qryExistingCType[0];

                    XElement currentWebCTypeDef = SPWebServicesClientFactory.XGetContentType(web, cTypeID);

                    XElement cTypeProperties = new XElement(cType);
                    cTypeProperties.RemoveNodes(); // gets rid of Field and FieldRef elements while keeping attributes
                    // do we need to remove stuff here???

                    XElement newFields = BuildContentTypesWebServiceFieldsNode(currentWebCTypeDef, cType, BuildWebServiceFieldsNodeType.NewFields);
                    XElement updateFields = BuildContentTypesWebServiceFieldsNode(currentWebCTypeDef, cType, BuildWebServiceFieldsNodeType.ExstingFields);
                    XElement deleteFields = null; // TODO: implement me - maybe

                    XElement result = SPWebServicesClientFactory.XUpdateContentType(web, cTypeID, cTypeProperties, newFields, updateFields, deleteFields);
                    // TODO parse result, ensure success...

                    // add the content type to the update queue, we'll fire the event off later
                    cTypesNeedUpdate.Add(cTypeName);
                } else { // if not found...
                    // WARNING: the following uses reflection to hack the content type
                    web.CreateContentType(cType.GetXmlNode());
                    // uh-oh... uuurrrrtttt! (that's a breaking/record scratching sound, btw))
                    // throw new NotSupportedException("Creation of content types through the web service is not supported due to inability to pass ID as a parameter during creation. Ensure that you have included an element file with the desired content type data, and that the element file matches the file passed to this reciever.");
                }


            } // for

            // Now we have updated the content types. If that succeeded, update the list ct's too.
            OnRefreshListContentTypes(web, cTypesNeedUpdate);
        }

        // TODO this could easily be made more efficient still, just by generating new and updated in the same call
        private static XElement BuildContentTypesWebServiceFieldsNode(
            XElement currentWebCTypeDef,
            XElement featureElementCTypeDef,
            BuildWebServiceFieldsNodeType typeOfFields
        ) {
            int methodNumber = (int)typeOfFields;
            XElement fields = new XElement("Fields");

            // determine if the fieldName in the element list is already in the web
            List<XElement> elementFields = featureElementCTypeDef.GetAllElementsOfType("FieldRef");
            foreach (XElement qryElementField in elementFields) {
                // cycle through all the fields in the element file...
                string qryFieldId = qryElementField.Attribute("ID").Value;
                //string queryExistingFieldById = string.Format(".//Field[@ID='{0}']", qryFieldId);
                //XmlNodeList qryExistingField = currentWebCTypeDef.SelectNodes(queryExistingFieldById); // nsmgr
                List<XElement> qryExistingField = (
                    from XElement field in currentWebCTypeDef.Descendants()
                    where field.Name.LocalName == "Field" && field.Attribute("ID").Value == qryFieldId
                    select field
                ).ToList();

                // fieldName was found on the web's fieldName list...
                bool existingField = (qryExistingField != null && qryExistingField.Count > 0);
                if (existingField && typeOfFields == BuildWebServiceFieldsNodeType.ExstingFields)
                    AppendFieldXml(fields, qryElementField, methodNumber);
                if (!existingField && typeOfFields == BuildWebServiceFieldsNodeType.NewFields)
                    AppendFieldXml(fields, qryElementField, methodNumber);
            }
            return fields;
        }

        #endregion

        #region Site Columns Stuff

        /// <summary>
        /// Calls a web service to ensure that site columns exist.
        /// </summary>
        /// <param name="web">
        /// Web you want to create site columns for, 
        /// or use SPSite.RootWeb for site collection level.
        /// </param>
        /// <param name="elementFeatureAndFile"></param>
        /// <returns></returns>
        public static void EnsureSiteColumns(SPWeb web, string elementFeatureAndFile) {
            XElement elementDoc = SPFeatureXmlTools.XGetConfigFile(elementFeatureAndFile);
            EnsureColumns(web, elementDoc);
        }

        /// <summary>
        /// Given an element file and a web, ensures the fields have been created.
        /// Uses web service, rather than provisioning directly through a feature,
        /// which allows for some interesting "hacks".
        /// </summary>
        /// <param name="web">The target web</param>
        /// <param name="elementDoc">XmlDocument of the element.xml file with Feature nodes</param>
        public static void EnsureColumns(SPWeb web, XElement elementDoc) {
            XElement currentFieldsDoc = SPWebServicesClientFactory.XGetColumns(web);
            XElement newColumns = BuildSiteColumnsWebServiceFieldsNode(currentFieldsDoc, elementDoc, BuildWebServiceFieldsNodeType.NewFields);
            XElement updateColumns = BuildSiteColumnsWebServiceFieldsNode(currentFieldsDoc, elementDoc, BuildWebServiceFieldsNodeType.ExstingFields);
            XElement deleteColumns = null; // BuildFieldsNode(xmlDoc, deleteFieldsXQuery, false);
            XElement result = SPWebServicesClientFactory.XUpdateColumns(web, newColumns, updateColumns, deleteColumns);
        }

        /// <summary>
        /// Builds an individual fieldName node that will be used as an argument for UpdateColumns
        /// web method. Creates either either new or existing Fields, based delta of fields between
        /// an element manifest and current site columns.
        /// </summary>
        /// <param name="currentWebFieldDefs">The fields xml that was returned by GetColumns web method.</param>
        /// <param name="featureElementsFieldDef">The xml document with the Field element definition</param>
        /// <param name="typeOfFields">Determines whether you want to add fields that already exist, or fields that are new.</param>
        /// <returns></returns>
        private static XElement BuildSiteColumnsWebServiceFieldsNode(
            XElement currentWebFieldDefs,
            XElement featureElementsFieldDef,
            BuildWebServiceFieldsNodeType typeOfFields
        ) {
            XElement fields = new XElement("Fields");
            //int methodNumber = (int)typeOfFields;
            int methodNumber = 0;

            // determine if the fieldName in the element list is already in the web
            // I find this code so damn frustrating to get working!!!
            List<XElement> qryElementFields = featureElementsFieldDef.GetAllElementsOfType("Field");;
            foreach (XElement qryElementField in qryElementFields) {
                // cycle through all the fields in the element file...
                string qryFieldName = qryElementField.Attribute("Name").Value;
                //string queryExistingFieldByName = string.Format("/Fields/Field[@Name='{0}']", qryFieldName); // + " or @DisplayName='{0}']"
                //XmlNodeList qryExistingField = currentWebFieldDefs.SelectNodes(queryExistingFieldByName); // nsmgr
                List<XElement> qryExistingField = (
                    from XElement field in currentWebFieldDefs.Descendants()
                    where field.Name.LocalName.Equals("Field", StringComparison.InvariantCulture) && field.Attribute("Name").Value == qryFieldName
                    select field
                ).ToList();
                // fieldName was found on the web's fieldName list...
                bool existingField = (qryExistingField != null && qryExistingField.Count > 0);
                if (existingField && typeOfFields == BuildWebServiceFieldsNodeType.ExstingFields)
                    AppendFieldXml(fields, qryElementField, methodNumber++);
                if (!existingField && typeOfFields == BuildWebServiceFieldsNodeType.NewFields)
                    AppendFieldXml(fields, qryElementField, methodNumber++);
            }
            return fields;
        }

        #endregion

        #region Applies to both Site Columns and Content Types

        /// <summary>
        /// Creates a single fieldName node for use in calling the UpdateColumns web service.
        /// </summary>
        /// <param name="fieldsDoc">XmlDocument that contains targetFieldsNode</param>
        /// <param name="targetFieldsNode">the Fields node that the fieldName will be appended into</param>
        /// <param name="methodNumber">A method number, caller must increment for each node</param>
        /// <param name="sourceField">The xml fieldName that will be imported (copied) to make the Field node</param>
        private static void AppendFieldXml(
            XElement targetFieldsNode,
            XElement sourceField,
            int methodNumber
        ) {
            if (targetFieldsNode == null || targetFieldsNode.Name.LocalName != "Fields")
                throw new Exception("targetFieldsNode has an unexpected value. You can only supply a Fields node.");
            string sourceFieldName = sourceField == null ? string.Empty : sourceField.Name.LocalName;
            if (!sourceFieldName.Equals("Field", StringComparison.InvariantCulture)
                && !sourceFieldName.Equals("FieldRef", StringComparison.InvariantCulture))
                throw new Exception("sourceField has an unexpected value. You can only append Field or FieldRef elements to this node list.");
            bool newIdGuid = false; // elements coming froma file already have a deterministic ID
            bool removeAttribs = true; // these attribs always cause trouble... I think...

            XElement method = new XElement("Method");
            method.SetAttributeValue("ID", methodNumber.ToString());
            targetFieldsNode.Add(method);

            // use ImportNode instead of CloneNode when you are copying between Xml documents
            XElement field = null;
            if (sourceFieldName.Equals("Field", StringComparison.InvariantCulture)) {
                field = new XElement(sourceField); 
            } else if (sourceFieldName.Equals("FieldRef", StringComparison.InvariantCulture)) {
                // this is provided to support content type definitions
                field = new XElement("Field");
                if (sourceField.Attribute("ID") != null)
                    field.SetAttributeValue("ID", sourceField.Attribute("ID").Value);
                if (sourceField.Attribute("Name") != null)
                    field.SetAttributeValue("Name", sourceField.Attribute("Name").Value);
                if (sourceField.Attribute("Required") != null)
                    field.SetAttributeValue("Required", sourceField.Attribute("Required").Value);
                if (sourceField.Attribute("Hidden") != null)
                    field.SetAttributeValue("Hidden", sourceField.Attribute("Hidden").Value);
                if (sourceField.Attribute("ReadOnly") != null)
                    field.SetAttributeValue("ReadOnly", sourceField.Attribute("ReadOnly").Value);
            }
            if (field == null || field.Name.LocalName != "Field")
                throw new Exception("xml node import returned an unexpected result. This node list should contain only Field elements.");
            // I thought you had to remove ID from the newFields, but I guess not...
            // see: http://sharepointandstuff.blogspot.com/2009/01/how-to-addupdatedelete-site-columns-by.html
            if (newIdGuid)
                field.SetAttributeValue("ID", Guid.NewGuid().ToString());
            if (removeAttribs) {
                if (field.Attribute("Version") != null) {
                    // removed because of problem described in http://www.sharepointblogs.com/abdrasin/archive/2007/10/31/site-column-update-error-the-object-has-been-updated-by-another-user-since-it-was-last-fetched.aspx
                    field.Attribute("Version").Remove();
                }
                // not sure if I should've removed these or not
                if (field.Attribute("StaticName") != null)
                    field.Attribute("StaticName").Remove(); 
                if (field.Attribute("SourceID") != null)
                    field.Attribute("SourceID").Remove(); 
            }
            method.Add(field);
        }

        #endregion

    }

    public enum BuildWebServiceFieldsNodeType {
        NewFields = 1,
        ExstingFields = 2,
        DeleteFields = 3
    }

} // namespace
